package com.ke.rule.base;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.*;
import java.util.concurrent.atomic.LongAdder;
import java.util.stream.Collectors;

/**
 * service method template class (The template design pattern is applied)<br>
 * <p>
 * The process of a typical interface can be divided into six parts, and the logic of the code can only be clear if each stage of the process is clearly defined and agreed upon.<br>
 * But in some cases, a and b might merge, and c might merge into b's logic. But we recommend a strict separation of steps.
 * 
 * @author guyu
 */
public abstract class ServiceMethodTemplate<R> implements IServiceMethod {
  private static final Logger logger = LoggerFactory.getLogger(ServiceMethodTemplate.class);
  
  // exec load counter
  private static final LongAdder counter = new LongAdder();

  private final IApplicationService service;
  private final String id;      // auto generated method instance id
  private MethodStepType currentStepType;   // current step type
  
  private boolean endSignaled;  // an early end sign
  private int stepRoute;        // executed step path
  private boolean idlingTest;   // idling test mode flag
  private List<FuncUnitSpec> executedFuncUnits;
  
  protected ServiceMethodTemplate(IApplicationService service) {
    super();
    Objects.requireNonNull(service, "service");

    this.service = service;
    this.id = newId(Objects.hash(service));
    this.currentStepType = null;
    this.endSignaled = false;
    this.stepRoute = 0;
    this.idlingTest = false;
    this.executedFuncUnits = new ArrayList<>();
  }

  public String getId() {
    return id;
  }

  public IApplicationService getService() {
    return service;
  }

  public MethodStepType getCurrentStepType() {
    return currentStepType;
  }
  
  /**
   * Get the steps of the route
   * @return
   */
  public MethodStepType[] getStepRoute() {
    return MethodStepType.map(stepRoute).toArray(new MethodStepType[0]);
  }
  
  /**
   * generate new instance id
   * @hash
   * @return
   */
  private String newId(int hashCode) {
    counter.increment();
    return String.format("%x-%x", hashCode, counter.longValue());
  }

  public abstract ServiceMethodContext getContext();

  /**
   * step forward
   * @param next step
   * @return
   */
  private ValidationException.Builder stepForward(MethodStepType next) {
    setCurrentStepType(next);
    if (isEarlyEnd()) {
      return null;
    }
    return proceedToStep();
  }
  
  protected void setCurrentStepType(MethodStepType step) {
    this.currentStepType = step;
    this.stepRoute = this.stepRoute | step.getValue();
  }
  
  /**
   * start and continue step
   * @return
   */
  protected ValidationException.Builder proceedToStep() {
    return null;
  }
  
  /**
   * Preprocessing, including building context, reorganizing input parameters, etc
   */
  protected void preprocess() {
    stepForward(MethodStepType.Preprocess);
  }
  
  /**
   * verify the validity and integrity of the parameters<br>
   * such as Null, Empty, Invalid Enumeration, Unexpected data type, Out of bounds etc.<br>
   * <p>
   * These checks can be done without querying any data from the database.
   * 
   * @return
   */
  protected ValidationException.Builder validateParameters() {
    return stepForward(MethodStepType.ValidateParameter);
  }
  
  /**
   * Prepare study data in advance for feasibility determination
   * Use ContextAwareRepo.getFieldValueSet to collect the key value set, then call loadBulk to loading data
   */
  protected void prepareStudyData() {
  }
  
  /**
   * determine the business feasibility of the operation<br>
   * such as duplicate names, object reference does not exist, cannot delete referenced object, business logic compliance etc.<br>
   * <p>
   * This step generally requires load data from the database and performing the necessary calculations and assembly.
   * 
   * @return
   */
  protected ValidationException.Builder determineFeasibility() {
    return stepForward(MethodStepType.DetermineFeasibility);
  }
  
  /**
   * business logic handling<br>
   * <p>
   * Generally is the memory computation, for example lock, the detail list data summary, the split line, the lines merge, the derivation computation and so on.
   * 
   */
  protected void handleBusinessLogic() {
    stepForward(MethodStepType.HandleBusinessLogic);
  }
  
  /**
   * persist objects to db or nosql
   * 
   */
  protected void dataPersistence() {
    stepForward(MethodStepType.DataPersistence);
  }
  
  /**
   * the processing operation after persistence is successful
   */
  protected ValidationException.Builder executePostProcessing() {
    return stepForward(MethodStepType.ExecutePostProcessing);
  }
  
  /**
   * perform the actions before exit (unlocking, resource release, etc)
   */
  protected void performActionOnExit() {
    stepForward(MethodStepType.PerformActionOnExit);
  }
  
  /**
   * signal to end, subsequent FuncUnits and Steps will be ignored
   */
  protected void signalToEnd() {
    this.endSignaled = true;
  }
  
  /**
   * is early end （end signaled and not the last step）
   * @return
   */
  public boolean isEarlyEnd() {
    return this.endSignaled && MethodStepType.PerformActionOnExit != this.currentStepType;
  }
  
  /**
   * invoke method implementation
   */
  public final R call() {
    MethodCallStack.enter(this);
    
    CallLatency.start(getClass().getSimpleName(), getId(), CallBreakdown.GROUP_METHOD);
    String traceId = SnippetsTrace.begin(false/*info level log*/);

    CallLatency.Breakdowns breakdowns = null;
    Throwable errorCause = null;
    R results = null;
    try {
      results = doWork();
    } catch (Throwable throwable) {
      // Throwable.fillStackTrace() will be called after logger.error
      logger.error(String.format("An exception occurs (step: %s): %s %s %s", this.currentStepType , this.getClass().getSimpleName(), throwable.getClass().getSimpleName(), ExceptionUtils.getStackTrace(throwable)));
      
      // Empty stackTraces to prevent the exception having too much stack trace information
      //throwable.setStackTrace(new StackTraceElement[0]);
      
      // dump BusinessException
      if (throwable instanceof BusinessException && ((BusinessException) throwable).anyProblems()) {
        logger.error("Dump business exception ...{}", System.lineSeparator(), throwable);
      }
      
      errorCause = throwable;
      ExceptionUtils.rethrow(throwable);
    } finally {
      // perform the operation before exit, regardless of success or failure
      try {
        performActionOnExit();
      } catch (Throwable thrw) {
        logger.error("failed to perform action on exit", thrw);
        
        // 之前的步骤如果已经抛出异常，则不再重复抛出
        if (null == errorCause) {
          ExceptionUtils.rethrow(thrw);
        }
      } finally {
        // do disponse
        getContext().dispose();
        MethodCallStack.exit();

        breakdowns = CallLatency.stop(getClass().getSimpleName(), getId());
      }
    }

    if (logger.isInfoEnabled()
        && (isIdlingTest() || (null != breakdowns && !breakdowns.isUltraFast()))) {
      logger.info("{} Latency Breakdown{}{}", this.getClass().getSimpleName(),
          System.lineSeparator(), breakdowns.prettyFormat(CallBreakdown.GROUP_FUNC_UNIT));
    }

    return results;
  }
  
  /**
   * go to work
   */
  protected R doWork() {
    // step0
    preprocess();
    
    // step1
    ValidationException.Builder builder = validateParameters();
    if (null != builder && builder.isPresent()) {
      builder.throwIfPresent();
    }
    
    // step2
    builder = determineFeasibility();
    if (null != builder && builder.isPresent()) {
      builder.throwIfPresent();
    }
    
    // After both parameters validation and business feasibility are passed, business logic processing and saving begins
    handleBusinessLogic();
    dataPersistence();

    builder = executePostProcessing();
    if (null != builder && builder.isPresent()) {
      builder.throwIfPresent();
    }
    
    return getResult();
  }
  
  /**
   * Gets the result of the method execution
   */
  @SuppressWarnings("unchecked")
  protected R getResult() {
    return (R) getContext().getReturnResult();
  }
  
  /**
   * track the FuncUnit that has been executed
   * @param list
   */
  protected void trackExecFuncUnits(List<FuncUnitSpec> list) {
    this.executedFuncUnits.addAll(list);
  }
  
  /**
   * 获取最后一次调用的方法实例
   * @return
   */
  public static IServiceMethod getLastCalled() {
    return MethodCallStack.getLastCalled();
  }
  
  public void setIdlingTest(boolean idlingTest) {
    this.idlingTest = idlingTest;
  }

  @Override
  public boolean isIdlingTest() {
    return this.idlingTest;
  }
  
  @Override
  public List<String> getExecutedFuncUnits() {
    return this.executedFuncUnits.stream().map(func -> func.getName()).collect(Collectors.toList());
  }
  
  @Override
  public boolean areFuncUnitsExecuted(String... funcUnits) {
    Objects.requireNonNull(funcUnits, "funcUnits");
    
    Set<String> actualFuncUnits = this.executedFuncUnits.stream().map(FuncUnitSpec::getName).collect(Collectors.toSet());
    if (logger.isInfoEnabled()) {
      logger.info("executed func units: {}, expected: {}", actualFuncUnits, funcUnits);
    }
    return Arrays.stream(funcUnits).allMatch(e -> actualFuncUnits.contains(e));
  }
}
